Skip to content

fix(web): restore styling under CSP, and give KB articles a detail page - #13

Open
TbusOS wants to merge 1 commit into
mainfrom
fix/kb-web-ui
Open

fix(web): restore styling under CSP, and give KB articles a detail page#13
TbusOS wants to merge 1 commit into
mainfrom
fix/kb-web-ui

Conversation

@TbusOS

@TbusOS TbusOS commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Closes #12.

1. Styling under CSP — per-response nonce

layout() emits one inline <style>; the response shipped
default-src 'self', which drops inline styles. The browser was therefore
rendering every page with its own defaults.

Each response now generates a fresh nonce, stamps it on the style block, and
sends a matching style-src 'nonce-...'. 'unsafe-inline' was deliberately
not used — it would restore the look while admitting any injected style,
which is the thing the header is there to stop.

_nonce_attr() rejects a nonce that is not base64url-safe, so a caller cannot
break out of the attribute.

Measured before/after on the same document:

body background CSS rules sidebar
before transparent 0 1184px
after rgb(250,249,245) 36 220px

No CSP violations reported by the browser after the change.

2. KB article detail page

  • /kb/<topic> route, mirroring the existing /memory/ pattern.
  • render_kb_detail() — chapter bodies, a table of contents built from each
    chapter's headings, and a metadata panel (lifecycle / scope / author /
    chapters / word count) with a digest freshness badge.
  • /kb list titles now link to their article.
  • web/markdown.py — a small renderer for the constructs the chapter guide
    asks authors to use: headings with stable anchors, paragraphs, fenced code,
    tables, lists, block quotes, rules, and the inline set. Not a CommonMark
    implementation, and it says so; anything outside that set renders as text
    rather than being guessed at.

Input treated as untrusted

Both the topic in the URL and the chapters: entries in README.md are
author-controlled and are constrained to the article directory. A chapter entry
containing a separator or a leading dot is skipped; a topic that is not a plain
directory name yields 404. An unreadable article and a missing one both answer
404, so the response does not disclose directory shape.

Every renderer branch escapes before emitting. Code spans are lifted out before
the emphasis pass so their contents are never reinterpreted. Link targets that
are not same-document, relative, or http(s) degrade to their label — a
javascript: target never becomes an href.

Tests

21 added: nonce/CSP pairing, per-response freshness, absence of
'unsafe-inline', malformed-nonce rejection, detail rendering, list linking,
404 paths, traversal refusal (parametrised), chapter-content escaping,
out-of-directory chapter entries, and the markdown block/inline/safety set.

Full unit run — identical 36 pre-existing failures before and after
(test_scope, test_pool_git_sync, test_observer_install,
test_canonical_uri, test_task_hash; all subprocess/git related and unrelated
to this change). Passing count 1355 → 1376.

Not in this change

The design mockups under docs/design/ carry a full system
(anthropic.css + app.css, ~36 KB) that the served UI does not use. Adopting
it means serving static assets rather than inlining, which is a larger change
and a separate discussion. This PR makes the article readable within the
existing style vocabulary; it does not reproduce the mockup.

Two defects made the web UI unusable for its Knowledge Base.

First, every page rendered with no styling. All CSS is one inline <style>
emitted by layout(), and the response carried `default-src 'self'` — a policy
that drops inline styles unless they are allowed by 'unsafe-inline' or matched
by a nonce. Measured: 0 CSS rules applied under the shipped header, 36 under
either 'unsafe-inline' or no header at all. The same HTML opened over file://
was always fine, which is why this never showed up outside a browser hitting
the server. Each response now carries a fresh nonce and a matching
style-src 'nonce-...'. Reaching for 'unsafe-inline' would have fixed the symptom
while admitting any injected style — the opposite of what the header is there for.

Second, KB articles had no detail page. /kb listed them, the titles were not
links, and no /kb/<topic> route existed, so a reader could see a row and nothing
else — while TASKS lists T-119 (KB Article page) as done. There is now a detail
route rendering chapters, a table of contents built from the headings, and a
metadata panel, plus web/markdown.py: a small renderer covering the constructs
the chapter guide asks for. It escapes before emitting on every branch, keeps
code spans out of the emphasis pass, and refuses link targets that are not
same-document or http(s).

Chapter names from `chapters:` and the topic from the URL are both
author-controlled, so both are constrained to the article directory.

21 tests added (nonce/CSP pairing, detail rendering, traversal refusal,
escaping, markdown blocks and inline). Full unit run: same 36 pre-existing
failures before and after, 1355 -> 1376 passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web UI renders with no styling at all, and KB articles have no detail page

1 participant